package calao.storage.bdb;

import java.io.PrintWriter;

import com.sleepycat.je.DatabaseException;

import calao.storage.FieldCondition;
import calao.storage.Field;
import calao.storage.Index;
import calao.storage.Record;
import calao.util.Cursor;
import calao.util.Printable;
import static calao.util.Logger.*;

class TableCursor 
implements Cursor<Record>, Printable
{
	
	static int nextId = 1;

	static class FetchCond {
		final FetchCond next;
		final FieldCondition fieldCond;
		FetchCond(FieldCondition fieldCond, FetchCond next) 
		{
			this.fieldCond = fieldCond;
			this.next = next;
		}
	}
	
	static class FieldOptions 
	{
		// conditions that must be met for the record to be fetched
		FetchCond firstFetchCond;
		// whether to skip the field
		boolean skipped = false; 
		void addFetchCond(FieldCondition fieldCond) {
			firstFetchCond = new FetchCond(fieldCond, firstFetchCond);
		}
	}

	final int id = nextId++;
	final TableDatabase tdb;
	final FieldOptions[] options;
	
	Index index;
	int indexUsedFieldCount;
	int rcount = 0; // count of records retrieved by this cursor so far
	Record current;
	
	boolean distinctOn = false;
	boolean hasFetchCond = false;
	boolean closed = false;
	KeySet fetchedKeys; 

	protected BdbCursor bdbCursor;

	TableCursor(TableDatabase tdb)
	{
		this.tdb = tdb;
		options = new FieldOptions[tdb.table.getFieldCount()];
		index = tdb.primaryIndex;
	}
	
	void setDistinct(boolean trueOrFalse)
	{
		distinctOn = trueOrFalse;
		if (distinctOn) fetchedKeys = new KeySet(tdb);
	}
	
	void use(Index index, int usedFieldCount)
	{
		this.index = index;
		this.indexUsedFieldCount = usedFieldCount;
	}
	
	TableCursor(TableDatabase tdb, Iterable<FieldCondition> fetchConds)
	{
		this(tdb);
		addFetchConditions(fetchConds);
	}

	private BdbCursor newBdbCursor()
	{
		if (index==null || index==tdb.primaryIndex) return new PrimaryCursor(tdb.db);
		else return new SecondaryCursor(tdb.storage.indexDbMap.get(index).sdb);
	}

	/**
	 * Returns true if the value meets all fetch conditions for the specified field.
	 */
	boolean meetsFetchConditions(Field field, Object value)
	{
		FieldOptions opts = options[field.getPosition()];
		if (opts!=null) {
			FetchCond cond = opts.firstFetchCond; 
			while (cond!=null) {
				if (!cond.fieldCond.matches(value)) return false; 
				cond = cond.next;
			}
		}
		return true;
	}

	/** 
	 * Returns true if at least one of the values meets all fetch conditions.
	 */
	boolean meetsFetchConditions(Field field, Object[] values)
	{
		FieldOptions opts = options[field.getPosition()];
		if (opts!=null) {
			FetchCond firstCond = opts.firstFetchCond; 
			if (firstCond!=null) {
				if (values.length==0) {
					FetchCond cond = firstCond;
					while (cond!=null) {
						if (!cond.fieldCond.matches(null)) return false;
						cond = cond.next;
					}
				}
				else {
					valuesLoop: for (int v=0; v<values.length; v++) 
					{
						FetchCond cond = firstCond;
						while (cond!=null) {
							if (!cond.fieldCond.matches(values[v])) {
								continue valuesLoop;
							}
							cond = cond.next;
						}
						return true;
					}
					return false;
				}
			}
		}
		return true;
	}

	boolean skips(Field field)
	{
		FieldOptions opts = options[field.getPosition()];
		return opts!=null && opts.skipped;
	}
	
	void skip(Field field)
	{
		getOrCreateOptions(field).skipped = true;
	}

	boolean hasFetchCondition(Field field)
	{
		FieldOptions opts = options[field.getPosition()];
		return opts!=null && opts.firstFetchCond!=null;
	}
	
	void addFetchCondition(FieldCondition fieldCond)
	{
		hasFetchCond = true;
		FieldOptions opts = getOrCreateOptions(fieldCond.getField());
		opts.addFetchCond(fieldCond);
	}

	void addFetchConditions(Iterable<FieldCondition> fieldConds)
	{
		hasFetchCond = true;
		for (FieldCondition fieldCond : fieldConds) {
			FieldOptions opts = getOrCreateOptions(fieldCond.getField());
			opts.addFetchCond(fieldCond);
		}
	}
	
	private FieldOptions getOrCreateOptions(Field field)
	{
		FieldOptions opts = options[field.getPosition()];
		if (opts==null) {
			opts = new FieldOptions();
			options[field.getPosition()] = opts;
		}
		return opts;
	}
			
	protected boolean nextBdbHit() 
	{
		if (bdbCursor.hitCount==0) return bdbCursor.getFirst();
		else return bdbCursor.getNext();
	}
	
	final public void close() {
		if (!closed) {
			bdbCursor.close();
			closed = true;
			log(DEBUG, "Cursor %s (%s) on %s fetched %d record(s) out of %d database hit(s)",
					id, getTypeName(), tdb.table.getName(), rcount, bdbCursor.hitCount);
		}
	}

	final public Record get() {
		return current;
	}
	
	final public boolean next() {
		assertBdbCursor();
		while (nextBdbHit()) {
			current = getBdbHitRecord();
			if (current!=null) return true;
		}
		return false;
	}
	
	// TODO skip record data when skipping all non-primary-key fields
	void init() { }
	
	@Override 
	protected void finalize()
	{
		close(); 
	}

	public long skipNext(long count)
	{
		if (count<0) throw new IllegalArgumentException();
		current = null;
		assertBdbCursor();
		boolean skipData = bdbCursor.getSkipData();
		if (!hasFetchCond) bdbCursor.setSkipData(true);
		long scount = 0;
		while (scount<count && nextBdbHit()) {
			if ((!hasFetchCond) || getBdbHitRecord()!=null) scount++;
		}
	    if (!hasFetchCond) bdbCursor.setSkipData(skipData);
		return scount;
	}
	
	private Record getBdbHitRecord()
	{
		CursorReader reader = new CursorReader(this);
		if ((!reader.readRecord(bdbCursor.getHitKey(), bdbCursor.getHitData()))
				&& ((!distinctOn) || fetchedKeys.addKey(reader.record))) 
		{
			rcount++;
			return reader.record;
		}
		return null;
	}
		
	private void assertBdbCursor()
	{
		if (bdbCursor==null) {
			bdbCursor = newBdbCursor();
			init();
		}				
	}
	
	int getBdbHitCount()
	{
		return bdbCursor!=null? bdbCursor.hitCount : 0;
	}
	
	int getBdbFetchCount()
	{
		return bdbCursor!=null? bdbCursor.fetchCount : 0;
	}
	
	public void delete()
	{
		if (!bdbCursor.delete()) {
			log(ERROR, "Cursor delete failed");
		}
	}

	public void printTo(PrintWriter writer)
	{
		int count = 0;
		while (next()) {
			get().printTo(writer);
			writer.println();
			count++;
		}
		writer.format("%d records", count);
	}

	public void set(Record value) 
	{
		RecordWriter writer = new RecordWriter(tdb.table);
		try {
			writer.writeData(value);
		}
		catch (DatabaseException e) {
			throw new RuntimeException(e);
		}
		if (!bdbCursor.set(writer.dataEntry)) {
			log(ERROR, "Cursor set failed");
		}
	}
	
	String getTypeName()
	{
		return "default";
	}

}
